use std::{convert::Infallible, future::Future, path::PathBuf, pin::Pin, sync::Arc};

use http::{HeaderName, HeaderValue, StatusCode};
use http_body_util::Empty;
use std::fmt::Debug;
use std::io;
use tower_http::services::{fs::ServeDir, ServeFile};
use tower_service::Service;

use crate::{encore::parser::meta::v1 as meta, model::RequestData};

use super::{BoxedHandler, Error, HandlerRequest, ResponseData};

#[derive(Clone, Debug)]
pub struct StaticAssetsHandler {
    service: Arc<dyn FileServer>,
    not_found_handler: bool,
    not_found_status: StatusCode,
    headers: Vec<(HeaderName, HeaderValue)>,
}

impl StaticAssetsHandler {
    pub fn new(cfg: &meta::rpc::StaticAssets) -> Self {
        let service = ServeDir::new(PathBuf::from(&cfg.dir_rel_path));

        let not_found_status = cfg
            .not_found_status
            .and_then(|c| StatusCode::from_u16(c as u16).ok())
            .unwrap_or(StatusCode::NOT_FOUND);

        let not_found = cfg
            .not_found_rel_path
            .as_ref()
            .map(|p| ServeFile::new(PathBuf::from(p)));
        let not_found_handler = not_found.is_some();
        let service: Arc<dyn FileServer> = match not_found {
            Some(not_found) => Arc::new(service.not_found_service(not_found)),
            None => Arc::new(service),
        };

        let headers: Vec<(HeaderName, HeaderValue)> = cfg
            .headers
            .iter()
            .flat_map(|(key, header_values)| {
                HeaderName::from_bytes(key.as_bytes())
                    .inspect_err(|e| {
                        log::error!("skipping header: '{}' - {}", key, e);
                    })
                    .ok()
                    .map(|header_name| {
                        header_values.values.iter().filter_map(move |value| {
                            HeaderValue::from_bytes(value.as_bytes())
                                .inspect_err(|e| {
                                    log::error!("skipping header '{}': '{}' - {}", key, value, e);
                                })
                                .ok()
                                .map(|header_value| (header_name.clone(), header_value))
                        })
                    })
                    .into_iter()
                    .flatten()
            })
            .collect();

        StaticAssetsHandler {
            service,
            not_found_handler,
            not_found_status,
            headers,
        }
    }
}

impl BoxedHandler for StaticAssetsHandler {
    fn call(
        self: Arc<Self>,
        req: HandlerRequest,
    ) -> Pin<Box<dyn Future<Output = ResponseData> + Send + 'static>> {
        Box::pin(async move {
            let RequestData::RPC(data) = &req.data else {
                return ResponseData::Typed(Err(Error::internal(anyhow::anyhow!(
                    "invalid request data type"
                ))));
            };

            // Find the file path from the request.
            let file_path = match &data.path_params {
                Some(params) => params
                    .values()
                    .next()
                    .and_then(|v| v.as_str())
                    .map(|s| format!("/{s}"))
                    .unwrap_or("/".to_string()),
                None => "/".to_string(),
            };

            let httpreq = {
                let mut b = axum::http::request::Request::builder();
                {
                    // Copy headers into request.
                    let headers = b.headers_mut().unwrap();
                    for (k, v) in &data.req_headers {
                        headers.append(k.clone(), v.clone());
                    }
                }
                match b
                    .method(data.method)
                    .uri(file_path)
                    .body(Empty::<bytes::Bytes>::new())
                {
                    Ok(req) => req,
                    Err(e) => {
                        return ResponseData::Typed(Err(Error::invalid_argument(
                            "invalid file path",
                            e,
                        )));
                    }
                }
            };

            match self.service.serve(httpreq).await {
                Ok(mut resp) => {
                    let resp_headers = resp.headers_mut();
                    for (name, value) in &self.headers {
                        resp_headers.append(name.clone(), value.clone());
                    }

                    match resp.status() {
                        // 1xx, 2xx, 3xx are all considered successful.
                        code if code.is_informational()
                            || code.is_success()
                            || code.is_redirection() =>
                        {
                            ResponseData::Raw(resp.map(axum::body::Body::new))
                        }
                        axum::http::StatusCode::NOT_FOUND => {
                            // If we have a not found handler, use that directly.
                            if self.not_found_handler {
                                *resp.status_mut() = self.not_found_status;
                                ResponseData::Raw(resp.map(axum::body::Body::new))
                            } else {
                                // Otherwise return our standard not found error.
                                ResponseData::Typed(Err(Error::not_found("file not found")))
                            }
                        }
                        axum::http::StatusCode::METHOD_NOT_ALLOWED => {
                            ResponseData::Typed(Err(Error {
                                code: super::ErrCode::InvalidArgument,
                                internal_message: None,
                                message: "method not allowed".to_string(),
                                stack: None,
                                details: None,
                            }))
                        }
                        axum::http::StatusCode::INTERNAL_SERVER_ERROR => {
                            ResponseData::Typed(Err(Error {
                                code: super::ErrCode::Internal,
                                internal_message: None,
                                message: "failed to serve static asset".to_string(),
                                stack: None,
                                details: None,
                            }))
                        }
                        code => ResponseData::Typed(Err(Error::internal(anyhow::anyhow!(
                            "failed to serve static asset: {}",
                            code,
                        )))),
                    }
                }
                Err(e) => ResponseData::Typed(Err(Error::internal(e))),
            }
        })
    }
}

trait FileServer: Sync + Send + Debug {
    fn serve(
        &self,
        req: axum::http::Request<Empty<bytes::Bytes>>,
    ) -> Pin<Box<dyn Future<Output = Result<FileRes, io::Error>> + Send + 'static>>;
}

type FileReq = axum::http::Request<Empty<bytes::Bytes>>;
type FileRes = axum::http::Response<tower_http::services::fs::ServeFileSystemResponseBody>;

impl<F> FileServer for ServeDir<F>
where
    F: Service<FileReq, Response = FileRes, Error = Infallible>
        + Debug
        + Clone
        + Sync
        + Send
        + 'static,
    F::Future: Send + 'static,
{
    fn serve(
        &self,
        req: axum::http::Request<Empty<bytes::Bytes>>,
    ) -> Pin<Box<dyn Future<Output = Result<FileRes, io::Error>> + Send + 'static>> {
        let mut this = self.clone();
        Box::pin(async move { this.try_call(req).await })
    }
}
